package org.smarthings.things.automation.engine;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import javax.script.ScriptEngineManager;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.osgi.service.component.ComponentContext;
import org.smarthings.things.automation.Action;
import org.smarthings.things.automation.Condition;
import org.smarthings.things.automation.Rule;
import org.smarthings.things.automation.RuleCollection;
import org.smarthings.things.automation.RuleEngine;
import org.smarthings.things.automation.Trigger;
import org.smarthings.things.automation.cep.CEPEngineFacade;
import org.smarthings.things.automation.scripting.engine.ScriptingEngineFacade;
import org.smarthings.things.management.ThingsRegistry;
import org.smarthings.things.management.metadata.provider.ThingEventMetadataProvider;

//TODO synchronization

@Component(immediate = true)
@Service(RuleEngine.class)
public class RuleEngineCore implements RuleEngine {

	@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MANDATORY_UNARY)
	private ThingsRegistry thingsRegistry;

	@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MANDATORY_UNARY)
	private ThingEventMetadataProvider thingEventMetaDataProvider;

	@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MANDATORY_UNARY)
	private ScriptEngineManager scriptEngineManager;

	private CEPEngineFacade cepEngineFacade;
	private ScriptingEngineFacade scriptingEngineFacade;
	private ThingsRegistryFacade thingsRegistryFacade;

	private Map<String, Rule> rules = new HashMap<String, Rule>(20);

	public void bindThingsRegistry(ThingsRegistry thingsRegistry) {
		this.thingsRegistry = thingsRegistry;
		this.thingsRegistryFacade = new ThingsRegistryFacade(thingsRegistry);
	}

	public void unbindThingsRegistry(ThingsRegistry thingsRegistry) {
		this.thingsRegistry = null;
		this.thingsRegistryFacade = null;
	}

	public void bindThingEventMetaDataProvider(ThingEventMetadataProvider thingEventMetaDataProvider) {
		this.thingEventMetaDataProvider = thingEventMetaDataProvider;
	}

	public void unbindThingEventMetaDataProvider(
			ThingEventMetadataProvider thingEventMetaDataProvider) {
		this.thingEventMetaDataProvider = null;
	}

	public void bindScriptEngineManager(ScriptEngineManager scriptEngineManager) {
		this.scriptEngineManager = scriptEngineManager;
		this.scriptingEngineFacade = new ScriptingEngineFacade(scriptEngineManager);
	}

	public void unbindScriptEngineManager(ScriptEngineManager scriptEngineManager) {
		this.scriptEngineManager = scriptEngineManager;
	}

	public void activate(ComponentContext context) {
		this.cepEngineFacade = new CEPEngineFacade();
		this.cepEngineFacade.activate(thingEventMetaDataProvider, context);
		this.scriptingEngineFacade.activate(thingsRegistryFacade);
	}

	public void deactivate(ComponentContext context) {
		this.cepEngineFacade.deactivate(context);
		this.scriptingEngineFacade.deactivate(context);
	}

	// register the statement to esper, store the rule
	public Rule registerRule(String name, String[] tags, Trigger trigger, Condition condition,
			Action action) {
		validateTrigger(trigger);
		validateCondition(condition);
		validateAction(action);

		Rule rule = RuleBuilder.create().name(name).tags(tags).trigger(trigger)
				.condition(condition).action(action).build();

		rules.put(rule.getUUID(), rule); // TODO store persistently
		cepEngineFacade.registerEventProcessingStatement(trigger.getExpression(), rule.getUUID(),
				new CEPStatementListener(rule, scriptingEngineFacade));

		return rule;
	}

	public void unregisterRule(String ruleUUID) {
		rules.remove(ruleUUID);
		cepEngineFacade.unregisterEventProcessingStatement(ruleUUID);
	}

	public Rule getRule(String ruleUUID) {
		return rules.get(ruleUUID);
	}

	@Override
	public void triggerRule(String ruleUUID) {

		// TODO

	}

	public RuleCollection listRules() {
		return new RuleCollectionImpl(new ArrayList<Rule>(rules.values()));
	}

	public RuleCollection filterRules(String filter) {
		return new RuleCollectionImpl(new ArrayList<Rule>(rules.values()));// TODO parse ldap!
	}

	private void validateTrigger(Trigger trigger) throws IllegalArgumentException {
		if (trigger == null || trigger.getExpression() == null
				|| trigger.getExpression().length() == 0) {
			throw new IllegalArgumentException("Invalid trigger : " + trigger);
		}
	}

	private void validateCondition(Condition condition) throws IllegalArgumentException {
		if (condition == null) {
			throw new IllegalArgumentException("Invalid condition : " + condition);
		}
		String conditionLanguage = condition.getLanguage();
		String conditionScript = condition.getScript();
		if (conditionLanguage == null || conditionLanguage.length() == 0) {
			throw new IllegalArgumentException("Invalid script language : " + conditionLanguage);
		}
		if (!scriptingEngineFacade.supports(conditionLanguage)) {
			throw new IllegalArgumentException("Unsupported script language : " + conditionLanguage);
		}
		if (conditionScript.length() == 0) {
			// do syntax check on the script based on the language?
			throw new IllegalArgumentException("Invalid condition script : " + conditionScript);
		}

	}

	private void validateAction(Action action) throws IllegalArgumentException {
		if (action == null) {
			throw new IllegalArgumentException("Invalid action : " + action);
		}
		String actionLanguage = action.getLanguage();
		String actionScript = action.getScript();
		if (actionLanguage == null || actionLanguage.length() == 0) {
			throw new IllegalArgumentException("Invalid script language : " + actionLanguage);
		}
		if (!scriptingEngineFacade.supports(actionLanguage)) {
			throw new IllegalArgumentException("Unsupported script language : " + actionLanguage);
		}
		if (actionScript == null || actionScript.length() == 0) {
			// do syntax check on the script based on the language?
			throw new IllegalArgumentException("Invalid action script : " + actionScript);
		}

	}

}
